﻿using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace ExpressionMapper
{
    using MappingExpressionGenerator = Func<Expression, Type, Expression, ParameterExpression, Expression>;

    /// <summary>
    /// Returns generator for Dictionary to Dictionary mappings.
    /// </summary>
    public static partial class Generators
    {
        public static MappingExpressionGenerator CollectionToCollection(MappingExpressionGenerator parent)
        {
            return (fromExpr, to, customMapping, dictionary) =>
                   (IsEnumerable(fromExpr.Type) && ImplementsCollection(to))
                       ? customMapping ?? GenerateCollectionConversionExpression(parent, fromExpr, to, dictionary)
                       : null;
        }

        private static Expression GenerateCollectionConversionExpression(MappingExpressionGenerator parent, Expression fromExpr,
                                                                Type to, ParameterExpression dictionary)
        {
            Debug.WriteLine("\tEnumerableToCollection ({0}): {1} to {2}", fromExpr.NodeType, fromExpr.Type.Name,
                                                 to.Name);

            Type fromElementType = GetCollectionElementType(fromExpr.Type);
            Type toElementType = GetCollectionElementType(to);

            Type converterType = typeof(Converter<,>).MakeGenericType(fromElementType, toElementType);

            ParameterExpression p = Expression.Parameter(fromElementType, "item");
            LambdaExpression elementLambda = Expression.Lambda(converterType, parent(p, toElementType, null, dictionary), p);



            ParameterExpression result = Expression.Variable(to, "result");
            MethodInfo add = to.GetMethod("Add", BindingFlags.Public | BindingFlags.Instance);
            var ctor = GetParameterizedConstructor(to);
            List<Expression> conversionBody = new List<Expression>();

            if (ctor != null)
            {
                conversionBody.Add(Expression.Assign(result, Expression.New(ctor, fromExpr)));
            }
            else
            {
                ctor = GetParameterlessConstructor(to);
                if (ctor == null)
                    throw new InvalidOperationException(string.Format("Could not find relevant constructor for type {0}", to.FullName));

                conversionBody.Add(Expression.Assign(result, Expression.New(ctor)));

                var breakLoop = Expression.Label("breakLoop");
                MethodInfo getEnumerator = fromExpr.Type.GetMethod("GetEnumerator", BindingFlags.Public | BindingFlags.Instance);
                MethodInfo moveNext = getEnumerator.ReturnType.GetMethod("MoveNext", BindingFlags.Public | BindingFlags.Instance);
                PropertyInfo current = getEnumerator.ReturnType.GetProperty("Current", BindingFlags.Public | BindingFlags.Instance);
                var enumerator = Expression.Variable(getEnumerator.ReturnType);
                conversionBody.Add(Expression.Block(new[] { enumerator },
                    Expression.Assign(enumerator, Expression.Call(fromExpr, getEnumerator)),
                    Expression.Loop(
                        Expression.Block(new List<Expression>{
                            Expression.IfThen(Expression.Not(enumerator,moveNext), Expression.Break(breakLoop)),
                            Expression.Call(result, add, Expression.Invoke(elementLambda, Expression.Property(enumerator, current))),
                                }),
                            breakLoop
                        )
                    ));
                Expression.Invoke(elementLambda, Expression.Property(enumerator, current));

                
            }
            conversionBody.Add(result);
            Expression.Block(new[] { result }, conversionBody);

            MethodInfo convertAllMethod = typeof(Generators)
                .GetMethod("ConvertDictionary", BindingFlags.Static | BindingFlags.Public)
                .MakeGenericMethod(fromElementType, toElementType);

            return Expression.Call(convertAllMethod, fromExpr, elementLambda);
        }

        private static ConstructorInfo GetParameterizedConstructor(Type targetType)
        {
            return targetType.GetConstructors().FirstOrDefault(c => c.GetParameters().Length == 1);
        }

        private static ConstructorInfo GetParameterlessConstructor(Type targetType)
        {
            return targetType.GetConstructors().FirstOrDefault(c => c.GetParameters().Length == 0);
        }

        private static bool IsEnumerable(Type t)
        {
            return (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)) ||
                t == typeof(IEnumerable) ||
                typeof(IEnumerable).IsAssignableFrom(t) ||
                typeof(IEnumerable<>).IsAssignableFrom(t);
        }

        private static Type GetCollectionElementType(Type t)
        {
            if (t.IsGenericType)
                return t.GetGenericArguments()[0];
            if (t == typeof(BitArray))
                return typeof (bool);

            return typeof(object);
        }

        private static bool ImplementsCollection(Type t)
        {
            return ImplementsSimpleCollection(t) || ImplementsGenericCollection(t);
        }

        private static bool ImplementsSimpleCollection(Type t)
        {
            return !t.IsGenericType
                   && typeof(ICollection).IsAssignableFrom(t);
        }

        private static bool ImplementsGenericCollection(Type t)
        {
            return t.IsGenericType
                && typeof(ICollection<>).IsAssignableFrom(t.GetGenericTypeDefinition());
        }

     
        private static IEnumerable<TOutput> ConvertEnumerable<TInput, TOutput>(IEnumerable<TInput> collection
                                                                                           , ReferenceDictionary parent)
        {
            if (collection == null)
            {
                return null;
            }
            return collection.Select(item => Mapper.Map<TInput, TOutput>(item,parent)).ToList();
        }
    }
}
